package cn.zhaopin.starter.security.manager;

import cn.zhaopin.common.constant.LogConstants;
import cn.zhaopin.common.context.UserContextHolder;
import cn.zhaopin.starter.security.basic.CodeException;
import cn.zhaopin.starter.security.basic.UserContext;
import cn.zhaopin.starter.security.cache.SecurityCacheUtil;
import cn.zhaopin.starter.security.common.JWTUtils;
import cn.zhaopin.starter.security.common.SecurityConstant;
import cn.zhaopin.starter.security.properties.JwtProperties;
import cn.zhaopin.starter.security.user.SecurityContextHolderBuilder;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.ReactiveAuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authorization.AuthorizationContext;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Objects;

/**
 * Description: api token验证
 *
 * @author zuomin (myleszelic@outlook.com)
 * @date: 2021/10/28-16:40
 */
@Slf4j
public class ApiTokenReactiveAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    private final SecurityCacheUtil securityCacheUtil;

    private final JwtProperties jwtProperties;

    public ApiTokenReactiveAuthorizationManager(SecurityCacheUtil securityCacheUtil, JwtProperties jwtProperties) {
        this.securityCacheUtil = securityCacheUtil;
        this.jwtProperties = jwtProperties;
    }

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
        ServerWebExchange exchange = authorizationContext.getExchange();
        String token;

        ServerHttpRequest request = exchange.getRequest();

        String paramToken = resolveParamToken(request);
        String headerToken = resolveHeaderToken(request);

        if (StringUtils.hasLength(headerToken)) {
            token = headerToken;
            try {
                token = URLDecoder.decode(token, StandardCharsets.UTF_8.displayName());
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            if (!token.startsWith(SecurityConstant.HEAD)) {
                throw CodeException.builder().code(40003).msg(request.getPath().value() + " miss bearer").build();
            }
            token = token.substring(SecurityConstant.HEAD.length());
        } else if (StringUtils.hasLength(paramToken)) {
            token = paramToken;
        } else {
            throw CodeException.builder().code(40003).msg(request.getPath().value() + " miss token").build();
        }

        Claims claims = JWTUtils.verifyJavaWebToken(token, jwtProperties.getSecretKey());
        if (Objects.isNull(claims)) {
            throw CodeException.builder().code(40003).msg(request.getPath().value() + " token error").build();
        }

        String userIdStr = claims.getSubject();
        if (!StringUtils.hasLength(userIdStr)) {
            throw CodeException.builder().code(40004).msg(request.getPath().value() + " invalid token").build();
        }

        Date expiredTime = claims.getExpiration();
        if (expiredTime == null || (System.currentTimeMillis() > expiredTime.getTime())) {
            throw CodeException.builder().code(40004).msg(request.getPath().value() + " jwt token expire").build();
        }

        String tokenCacheKey = JWTUtils.getTokenCacheKey(userIdStr, token);
        UserContext userContext = (UserContext) securityCacheUtil.get(tokenCacheKey);
        if (Objects.isNull(userContext)) {
            throw CodeException.builder().code(40004).msg(request.getPath().value() + " cache token expire").build();
        }
        userContext.setTokenKey(tokenCacheKey);

        //认证的token
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userContext, token, userContext.getAuthorities());

        //放入缓存
        SecurityContextHolderBuilder.build(authentication);

        // 存放在日志和本地缓存中
        MDC.put(LogConstants.LOG_USER_ID, userIdStr);
        UserContextHolder.setUserId(userIdStr);

        log.info("authenticated opt_user " + userIdStr + ", setting security context");

        return Mono.just(new AuthorizationDecision(true));
    }

    private String resolveHeaderToken(ServerHttpRequest request) {
        return request.getHeaders().getFirst(SecurityConstant.TOKEN_HEADER);
    }

    private String resolveParamToken(ServerHttpRequest request) {

        return request.getQueryParams().getFirst(SecurityConstant.TOKEN_PARAM);
    }
}
